import inherits from "inherits";

import CommandInterceptor from "diagram-js/lib/command/CommandInterceptor";

import {
  add as collectionAdd,
  remove as collectionRemove
} from "diagram-js/lib/util/Collections";

import { find } from "min-dash";

import { is } from "../../../util/ModelUtil";

var TARGET_REF_PLACEHOLDER_NAME = "__targetRef_placeholder";

/**
 * This behavior makes sure we always set a fake
 * DataInputAssociation#targetRef as demanded by the BPMN 2.0
 * XSD schema.
 *
 * The reference is set to a bpmn:Property{ name: '__targetRef_placeholder' }
 * which is created on the fly and cleaned up afterwards if not needed
 * anymore.
 *
 * @param {EventBus} eventBus
 * @param {BpmnFactory} bpmnFactory
 */
export default function DataInputAssociationBehavior(eventBus, bpmnFactory) {
  CommandInterceptor.call(this, eventBus);

  this.executed(
    [
      "connection.create",
      "connection.delete",
      "connection.move",
      "connection.reconnect"
    ],
    ifDataInputAssociation(fixTargetRef)
  );

  this.reverted(
    [
      "connection.create",
      "connection.delete",
      "connection.move",
      "connection.reconnect"
    ],
    ifDataInputAssociation(fixTargetRef)
  );

  function usesTargetRef(element, targetRef, removedConnection) {
    var inputAssociations = element.get("dataInputAssociations");

    return find(inputAssociations, function(association) {
      return (
        association !== removedConnection && association.targetRef === targetRef
      );
    });
  }

  function getTargetRef(element, create) {
    var properties = element.get("properties");

    var targetRefProp = find(properties, function(p) {
      return p.name === TARGET_REF_PLACEHOLDER_NAME;
    });

    if (!targetRefProp && create) {
      targetRefProp = bpmnFactory.create("bpmn:Property", {
        name: TARGET_REF_PLACEHOLDER_NAME
      });

      collectionAdd(properties, targetRefProp);
    }

    return targetRefProp;
  }

  function cleanupTargetRef(element, connection) {
    var targetRefProp = getTargetRef(element);

    if (!targetRefProp) {
      return;
    }

    if (!usesTargetRef(element, targetRefProp, connection)) {
      collectionRemove(element.get("properties"), targetRefProp);
    }
  }

  /**
   * Make sure targetRef is set to a valid property or
   * `null` if the connection is detached.
   *
   * @param {Event} event
   */
  function fixTargetRef(event) {
    var context = event.context,
      connection = context.connection,
      connectionBo = connection.businessObject,
      target = connection.target,
      targetBo = target && target.businessObject,
      newTarget = context.newTarget,
      newTargetBo = newTarget && newTarget.businessObject,
      oldTarget = context.oldTarget || context.target,
      oldTargetBo = oldTarget && oldTarget.businessObject;

    var dataAssociation = connection.businessObject,
      targetRefProp;

    if (oldTargetBo && oldTargetBo !== targetBo) {
      cleanupTargetRef(oldTargetBo, connectionBo);
    }

    if (newTargetBo && newTargetBo !== targetBo) {
      cleanupTargetRef(newTargetBo, connectionBo);
    }

    if (targetBo) {
      targetRefProp = getTargetRef(targetBo, true);
      dataAssociation.targetRef = targetRefProp;
    } else {
      dataAssociation.targetRef = null;
    }
  }
}

DataInputAssociationBehavior.$inject = ["eventBus", "bpmnFactory"];

inherits(DataInputAssociationBehavior, CommandInterceptor);

/**
 * Only call the given function when the event
 * touches a bpmn:DataInputAssociation.
 *
 * @param {Function} fn
 * @return {Function}
 */
function ifDataInputAssociation(fn) {
  return function(event) {
    var context = event.context,
      connection = context.connection;

    if (is(connection, "bpmn:DataInputAssociation")) {
      return fn(event);
    }
  };
}
